feat(web): add Cmd+F find in thread - #4599
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
ApprovabilityVerdict: Needs human review This PR introduces a new user-facing feature (Cmd+F find in thread) with new UI components, matching logic, and highlight painting infrastructure. New features with this scope of new behavior and components warrant human review. You can customize Macroscope's approvability policy. Learn more. |
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
… and pingdotgg#4599 The published build already carried pingdotgg#4605 and pingdotgg#4599 -- stack-build-info.json on t3code/stack lists both -- but the manifest edits that admitted them were never committed here, so the recorded intent had drifted from the artifact and a rebuild would have dropped two features. Restore them at the OIDs the published build resolved, then append pingdotgg#4624. pingdotgg#4624 is closed upstream without merging; carried because the replay cost is real on this phone. Its head carries a fix past the PR: the 200ms code-highlight debounces could never fire, because a growing code block's React key encodes its source offsets and the block remounts on every delta. Also refresh two pins that the artifact had moved past (the thread-picker group and local/nix-flake), and re-audit the exception set: the group rebuild at c5a4a73 shrank the CommandPalette losses on pingdotgg#4263/pingdotgg#4258/pingdotgg#4426 from 29/28/19 to 3/3/4. pingdotgg#4590's waiver is rewritten -- its 32 missing lines are an unbuilt branch revision, not a resolution loss, because the group is still pinned at the older member head.
7d951aa to
6a69b32
Compare
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
6a69b32 to
5ec0c9f
Compare
Adds an in-thread find bar to the chat timeline, bound to the new `chat.find` keybinding command (mod+f outside terminal focus). Matches are modeled against timeline entries rather than rendered DOM, so counts stay stable while the virtualized list mounts and unmounts rows, and a match inside a folded turn unfolds that turn on navigation. Highlights are painted with the CSS Custom Highlight API, which leaves the DOM untouched — markdown output, syntax-highlighted code blocks and copyable text are unaffected, and browsers without the API still get working counting and navigation. Refs pingdotgg#1486 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Length-preserving case fold: lowercasing that changes UTF-16 length
("İ" → "i" + combining dot) no longer drifts offsets into text nodes,
which could throw IndexSizeError from Range.setEnd. Code points whose
lowercase form is longer simply don't case-fold.
- Highlight painting is scoped to data-thread-find-text regions (user
message body, assistant markdown, plan markdown) so row chrome, fold
labels and work-log output can no longer light up for text the counter
never counted, or shift the active-occurrence index within a row.
- Find navigation calls onManualNavigation() so live follow cannot
scroll the timeline back to the streaming edge right after revealing
a match.
- The navigation identity now includes the query and a step nonce:
changing the query re-navigates even when the target row/occurrence
is unchanged, and Enter on a lone match re-reveals it.
- The post-scroll occurrence nudge retries across frames (and survives
streaming re-renders) instead of giving up if the row hasn't mounted
by the first requestAnimationFrame.
- Match count is reported via useLayoutEffect so the find bar can never
paint a total the step handlers aren't already using.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
70d719b to
f625827
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f625827. Configure here.
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts
# Conflicts: # apps/web/src/components/ChatView.tsx # apps/web/src/components/chat/MessagesTimeline.tsx # packages/contracts/src/keybindings.ts # packages/shared/src/keybindings.ts

What Changed
Adds in-thread find to the chat timeline, on the new
chat.findkeybinding command (mod+f,when: !terminalFocus, so it stays out of the terminal's own find).Enter/Shift+Enterstep forward and back,Esccloses.Scope is deliberately narrow: conversation content only — user messages, assistant messages and proposed plans. Work-log/tool rows are not searched.
apps/web/src/components/chat/threadFind.ts— pure match model over timeline entriesapps/web/src/components/chat/threadFindHighlights.ts— CSS Custom Highlight API paintingapps/web/src/components/chat/FindInThreadBar.tsx— the barapps/web/src/lib/visibleMessageText.ts— the "what does this user message display?" derivation, extracted fromMessagesTimelineso the row and find share one sourceWhy
Requested in #1486. Three earlier attempts (#1501, #3539, #3779) stalled; #1501 was closed with "it should restart as a smaller focused change". This is that smaller change, built to avoid the specific defects reviewers found in the earlier ones:
Highlighting never touches the DOM. Earlier attempts injected
<mark>tags into message text before markdown parsing, which rendered literal<mark class=...>inside fenced code blocks and corrupted copied source, and desynchronized the AST offsets that task-list checkbox toggling derives from message text. This uses the CSS Custom Highlight API instead: ranges are painted, the DOM is unmodified. Code blocks keep their syntax highlighting and their exact copy text. Browsers without the API render no highlights but keep working counting and navigation.Matches are counted against timeline entries, not rendered nodes.
LegendListvirtualizes the timeline, so DOM-derived counts change as you scroll. Counting from entries keeps the total stable, and navigation usesscrollToIndexon the row model rather thandocument.querySelector, so a match in an unmounted row is reachable.Folded turns unfold on navigation. A settled turn hides everything but its terminal assistant message. Those hidden messages are still counted, and stepping to one expands its turn first, then scrolls (screenshot below).
Only one occurrence is ever active. The active style applies to a single range, not to the whole row, so advancing within a long message moves the marker instead of lighting up every match in it.
The counter cannot show an impossible position. The index is clamped against the live match count on every render, so a streaming edit that removes matches cannot leave
5/2on screen. Occurrence scanning advances by the needle length, soaainaaacounts once — the count matches what a left-to-right highlighter can actually paint.Find searches what the timeline shows. User prompts carry appended
<terminal_context>/<element_context>/ preview-annotation blocks that are stripped before rendering. Searching rawmessage.textcounted invisible text; the display derivation is now shared between the row and the search index, so they cannot drift.Known limitation, called out rather than hidden: an occurrence split across text nodes by inline markup (
**bo**ld) is counted but not painted. The row is authoritative for navigation and the occurrence index within a row is clamped, so this degrades to "scrolls to the right message, paints fewer highlights than it counts" rather than breaking.UI Changes
Find bar with match count, live highlighting across markdown, inline code and fenced code blocks (light):
Dark:
Stepping onto a match inside a folded turn unfolds the turn and reveals it (the "Worked for 2m" fold above has expanded, and the commentary message it was hiding now shows the active match):
No results:
Verification
Unit tests:
Static checks:
Driven in a real browser against an isolated local environment seeded with a 17-turn thread (folded turns, a fenced code block, and enough filler to force virtualization). Confirmed there:
Ctrl+Fopens and focuses;deployreports1/12, matching a hand count that includes the folded commentary and the code block;Enterat1/12unfolds the folded turn and mounts the hidden message; wrap-around forward and back (12/12→1/12); the code block'stextContentis unchanged with zero<mark>elements in the DOM; a no-match query reports0/0and paints nothing;Esccloses and unregisters both highlights.Checklist
Note
Medium Risk
Touches large chat UI (
ChatView,MessagesTimeline) with scroll/live-follow and virtualization interactions; highlights degrade gracefully where the Custom Highlight API is missing.Overview
Adds in-thread find to the chat timeline via a new
chat.findcommand (mod+fwhen terminal and preview are not focused).ChatViewowns per-thread find state (query, active match, navigation) and rendersFindInThreadBarover the timeline.MessagesTimelinebuilds matches from timeline entries (user/assistant messages and proposed plans—not work logs), reports counts to the bar, unfolds folded turns and scrolls to the active row, and auto-expands collapsed user messages and plan cards while find is active.Highlights use the CSS Custom Highlight API (
threadFindHighlights) ondata-thread-find-textregions so markdown and copy text stay untouched. SharedderiveDisplayedUserMessageContentkeeps search text aligned with what the UI shows (stripped contexts). Unit tests cover matching, navigation, and highlight partitioning.Reviewed by Cursor Bugbot for commit ba5a3b3. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add Cmd+F find-in-thread search to the chat view
FindInThreadBaroverlay triggered byMod+F(when not in terminal or preview focus) via the newchat.findkeybinding command.Macroscope summarized ba5a3b3.